﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using OverStore.Runtime.PersistenceServiceProvider;
using OverStore.Runtime.Storage;
using OverStore.Runtime.ObjectRepository.ChangesTracker;

namespace OverStore.Runtime.ObjectRepository.ChangesTracker
{
    /// <summary>
    /// Base class implemented main functionallity of <see cref="IChangesTracker{TInstance}"/>.
    /// </summary>
    /// <typeparam name="TInstance">OverrideDbType of object wich changes may be tracked.</typeparam>
    /// <typeparam name="TIdentifier">OverrideDbType of object identifier.</typeparam>
    /// <typeparam name="TState">OverrideDbType of object represents object state.</typeparam>
    public abstract class ChangesTrackerBase<TInstance, TIdentifier, TState> : IChangesTracker<TInstance>
        where TInstance : class
        where TState : IEquatable<TState>
    {
        private readonly Dictionary<TIdentifier, TState> _Changes = new Dictionary<TIdentifier, TState>();
        private readonly Dictionary<TIdentifier, InstanceState> _InstanceStates = new Dictionary<TIdentifier, InstanceState>();
        private readonly Dictionary<TIdentifier, int> _DirtyInstances = new Dictionary<TIdentifier, int>();
        private readonly IPersistenceServiceProvider _Provider;

        /// <summary>
        /// Initializes new instance of <see cref="ChangesTrackerBase{TInstance, TIdentifier, TState}" /> class.
        /// </summary>
        /// <param name="provider">Allows to obtain instance of <see cref="IEntityRegistry{TInstance, TIdentifier}"/> to operate with
        /// instances and instance identifiers.</param>
        protected ChangesTrackerBase(IPersistenceServiceProvider provider)
        {
            if (Object.ReferenceEquals(provider, null))
                throw new ArgumentNullException("provider");
            _Provider = provider;
        }

        /// <summary>
        /// Get hash code of <see cref="IRecord"/> using specified field set.
        /// </summary>
        public static int CalculateDumpChecksum(TInstance instance, IEnumerable<string> fields)
        {
            Debug.Assert(instance != null, "Calculate checksum for null instance.");
            Debug.Assert(fields != null, "Calculating checksum with null fields");
            var result = 0;
            unchecked
            {
                foreach (var field in fields)
                {
                    var pi = typeof(TInstance).GetProperty(field);
                    if (pi == null)
                        continue;
                    object value = pi.GetValue(instance, null);
                    result = result + field.GetHashCode() + (value == null ? 0 : value.GetHashCode());
                }
            }

            return result;
        }

        /// <summary>
        ///     <para> Aquires the instance that does not exists in data storage.</para>
        ///     <para> If instance is not registered in changes tracker, it gets <see cref="InstanceState.Added"/> state.
        /// Otherwise, instance state still unchanged.</para>
        /// </summary>
        /// <param name="instance">The instance to aquire.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during changes tracker operation.</exception>
        public void CaptureNewInstance(TInstance instance)
        {
            Debug.Assert(instance != null, "Aquire null instance.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);

            if (!_InstanceStates.ContainsKey(id))
            {
                _InstanceStates[id] = InstanceState.Added;
                return;
            }

            var state = _InstanceStates[id];
            switch (state)
            {
                case InstanceState.Added:
                case InstanceState.Existing:
                case InstanceState.Foreign:
                    break;
                case InstanceState.MarkedAsDeleted:
                    _InstanceStates[id] = InstanceState.Existing;
                    break;
                case InstanceState.AddedThenDeleted:
                    _InstanceStates[id] = InstanceState.Added;
                    break;
            }
        }

        /// <summary>
        ///     <para> Notify changes tracker that object should be deleted on save.</para>
        ///     <para> If instance does not registered in the changes tracker previously or has <see cref="InstanceState.Added"/> state,
        /// it get <see cref="InstanceState.AddedThenDeleted"/> state. Otherwise, instance gets <see cref="InstanceState.MarkedAsDeleted"/> state. </para>
        /// </summary>
        /// <param name="instance">Object to be marked as deleted.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during method executing.</exception>
        public void MarkAsDeleted(TInstance instance)
        {
            Debug.Assert(instance != null, "Mark null instance as deleted");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            if (!_InstanceStates.ContainsKey(id))
                _InstanceStates[id] = InstanceState.MarkedAsDeleted;
            else
            {
                if (_InstanceStates[id] == InstanceState.Added || _InstanceStates[id] == InstanceState.Foreign)
                    _InstanceStates[id] = InstanceState.AddedThenDeleted;
                else
                    _InstanceStates[id] = InstanceState.MarkedAsDeleted;
            }
        }

        /// <summary>
        /// Marks instance as dirty. Changes Tracker reports instance data are changed on dirty instances, 
        /// even if data actually are not changed.
        /// </summary>
        /// <param name="instance">Object to mark as dirty.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during method execution.</exception>
        public void MarkAsDirty(TInstance instance)
        {
            Debug.Assert(instance != null, "Marks null instance as dirty.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            _DirtyInstances[id] = 1;
        }

        /// <summary>
        ///     <para> Accepts data changes made to specified instance and aquire current state of instance's data
        /// as new original instance data.</para>
        ///     <para> If instance state was <see cref="InstanceState.MarkedAsDeleted"/> or <see cref="InstanceState.AddedThenDeleted"/>,
        /// instance state become <see cref="InstanceState.Added"/>, otherwise state become <see cref="InstanceState.Existing"/>.</para>
        /// </summary>
        /// <param name="instance">The instance to accept changes of it.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during method executing.</exception>
        public void AcceptChanges(TInstance instance)
        {
            Debug.Assert(instance != null, "Accept Changes of null instance.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            if (_InstanceStates.ContainsKey(id) &&
                (_InstanceStates[id] == InstanceState.MarkedAsDeleted ||
                _InstanceStates[id] == InstanceState.AddedThenDeleted))
            {
                _InstanceStates.Remove(id);
                _Changes.Remove(id);
            }
            else
            {
                _InstanceStates[id] = InstanceState.Existing;
                _Changes[id] = GetObjectStateCore(instance);
            }
            _DirtyInstances.Remove(id);
        }

        /// <summary>
        /// Return <see cref="InstanceState"/> of specified object.
        /// </summary>
        /// <param name="instance">Instance to get state for.</param>
        /// <returns>
        /// The <see cref="InstanceState"/> of specified instance.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error occured during method executing.</exception>
        public InstanceState GetInstanceState(TInstance instance)
        {
            Debug.Assert(instance != null, "Get state of null instance.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            InstanceState result;
            if (!_InstanceStates.TryGetValue(id, out result))
            {
                _InstanceStates[id] = InstanceState.Added;
                return InstanceState.Added;
            }
            else
                return result;
        }

        /// <summary>
        /// Determines whether the data of specified instance were modified.
        /// </summary>
        /// <param name="instance">The instance to determine data modification.</param>
        /// <returns>
        ///     <c>true</c> if data of the specified instance was modifier; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error occured during changes tracker operation.</exception>
        public bool IsInstanceDataChanged(TInstance instance)
        {
            Debug.Assert(instance != null, "Determines whether data changed on null instance.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            if (_DirtyInstances.ContainsKey(id))
                return true;
            if (_Changes.ContainsKey(id))
            {
                var state = GetObjectStateCore(instance);
                return !_Changes[id].Equals(state);
            }
            else
                return true;
        }

        /// <summary>
        /// Update instance state and data changes state of specified instance corresponding to specified parameters.
        /// </summary>
        /// <param name="actualInstance">Actual instance registered in session which changes should be handled.</param>
        /// <param name="objectExistsInStorage"><c>true</c> if object exists in data storage; otherwise <c>false</c>.</param>
        /// <param name="instanceDataAreChanged"><c>true</c> if instance data are changed compare to data from storage.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during method executing.</exception>
        /// <exception cref="T:OverStore.Exceptions.PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void Attach(TInstance actualInstance, bool objectExistsInStorage, bool instanceDataAreChanged)
        {
            Debug.Assert(actualInstance != null, "Attaching null actual instance.");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(actualInstance);

            _InstanceStates[id] = objectExistsInStorage ? InstanceState.Existing : InstanceState.Added;
            if (instanceDataAreChanged)
                _Changes.Remove(id);
            else
                _Changes[id] = GetObjectStateCore(actualInstance);
        }

        /// <summary>
        /// When overrided in derived classes, returns object state of specified instance.
        /// </summary>
        /// <param name="instance">Instance wich state should be got.</param>
        /// <returns>
        /// Object represents state of <paramref name="instance"/>.
        /// </returns>
        protected abstract TState GetObjectStateCore(TInstance instance);
    }
}
